/**
 * Copyright 2008 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package kiff.dao.hibernate;

import java.lang.reflect.ParameterizedType;
import java.util.List;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TransactionRequiredException;

import kiff.dao.CoreDAO;
import kiff.exception.DataLayerException;
import kiff.util.Parameter;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.Projections;

import com.google.inject.Inject;

/**
 * Abstract Hibernate DAO, a convenience class that you can extend from to get a default set of Hibernate functionality.
 * @author Adam
 * @version $Id: AbstractHibernateDAO.java 73 2008-12-02 06:28:25Z a.ruggles $
 * @param <T> The entity type used as a return values in Generic methods.
 */
public abstract class AbstractHibernateDAO<T> implements CoreDAO<T> {
    /**
     * JPA Entity Manager.
     */
    protected EntityManager entityManager;

    /**
     * The Object Class that is being persisted.
     */
    protected Class<T> persistentClass;

    /**
     * Constructor that initializes the objectType.
     */
    @SuppressWarnings("unchecked")
    public AbstractHibernateDAO() {
        this.persistentClass = (Class<T>) ((ParameterizedType) getClass()
                .getGenericSuperclass()).getActualTypeArguments()[0]; 
    }

    /**
     * Constructor that injects the persistent class.
     * @param persistentClass The Class that is being persisted when extending this class.
     */
    protected AbstractHibernateDAO(final Class<T> persistentClass) {
        this.persistentClass = persistentClass;
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#delete(java.lang.Object)
     */
    public void delete(final T object) throws DataLayerException {
        try {
            getEntityManager().remove(object);
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (TransactionRequiredException transactionRequiredEx) {
            throw new DataLayerException("TransactionRequiredException", transactionRequiredEx);
        }
    }

    /**
     * Returns a list of objects of type T that match the criterion.
     * @param criterion The criterion used to find the T objects.
     * @return A list of objects of type T.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
    protected List<T> find(final Criterion... criterion) throws DataLayerException {
        try {
            Criteria criteria = getSession().createCriteria(getPersistentClass());
            for (Criterion c : criterion) {
                criteria.add(c);
            }
            return criteria.list();
        } catch (HibernateException hibernateEx) {
            throw new DataLayerException("HibernateException", hibernateEx);
        } catch (RuntimeException runtimeEx) {
            throw new DataLayerException("RuntimeException", runtimeEx);
        }
    }

    /**
     * Returns a list of objects of type T that match the detachedCriteria.
     * @param detachedCriteria The DetachedCriteria used to find the T objects.
     * @return A list of objects of type T.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
	protected List<T> find(final DetachedCriteria detachedCriteria) throws DataLayerException {
        try {
            Criteria searchCriteria = detachedCriteria.getExecutableCriteria(getSession());
            return searchCriteria.list();
        } catch (HibernateException hibernateEx) {
            throw new DataLayerException(hibernateEx);
        }
    }

    /**
     * Returns a {@link java.util.List} of results for a named query and parameters.
     * @param name The name of the query defined in metadata.
     * @param params A list of parameters for the named query.
     * @return A {@link java.util.List} of results of type T.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
	protected List<T> find(final String name, final Parameter ...params) throws DataLayerException {
    	try {
    		Query query = getEntityManager().createNamedQuery(name);
    		for (Parameter param : params) {
    			param.addParameter(query);
    		}
    		return query.getResultList();
    	} catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (NonUniqueResultException nonUniqueResultEx) {
        	throw new DataLayerException("NonUniqueResultException", nonUniqueResultEx);
        }  catch (NoResultException noResultEx) {
        	return null;
        }
    }

    /**
     * Returns a list of objects of type T that match the example object.
     * @param example The example object containing data you would like matched.
     * @param exclude An array of properties to exclude from the example.
     * @return A List of objects of type T.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
    protected List<T> find(final T example, final String[] exclude) throws DataLayerException {
        try {
            Criteria criteria = getSession().createCriteria(getPersistentClass());
            Example exampleQuery = Example.create(example);
            for (String property : exclude) {
                exampleQuery.excludeProperty(property);
            }
            criteria.add(exampleQuery);
            return criteria.list();
        } catch (HibernateException hibernateEx) {
            throw new DataLayerException("HibernateException", hibernateEx);
        } catch (RuntimeException runtimeEx) {
            throw new DataLayerException("RuntimeException", runtimeEx);
        }
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#findAll()
     */
    @SuppressWarnings("unchecked")
    public List<T> findAll() throws DataLayerException {
        try {
            return getEntityManager().createQuery("from " + getPersistentClass().getName()).getResultList(); 
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        }
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#findById(java.lang.Object)
     */
    public T findById(final Object id) throws DataLayerException {
        try {
            return getEntityManager().find(persistentClass, id);
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        }
    }

    /**
     * Convenience method used to return the number of records based on the Hibernate Criteria API.
     * @param detachedCriteria A <code>DetachedCriteria</code> object that specifies the search criterion.
     * @return The number of records that match the search criterion.
     * @throws DataLayerException If an error occurs in Hibernate or the JDBC driver.
     */
    protected int findCount(final DetachedCriteria detachedCriteria) throws DataLayerException {
        try {
            // Add Projection for the count.
            detachedCriteria.setProjection(Projections.rowCount());
            Criteria searchCriteria = detachedCriteria.getExecutableCriteria(getSession());
            Integer results = (Integer) searchCriteria.uniqueResult();
            if (results == null) {
                return 0;
            }
            return results.intValue();
        } catch (HibernateException hibernateEx) {
            throw new DataLayerException(hibernateEx);
        }
    }

    /**
     * Returns a single object of type T that matches the criterion. 
     * @param criterion The criterion used to find the object of type T.
     * @return A object of type T, or null.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
	protected T findResult(final Criterion... criterion) throws DataLayerException {
    	try {
            Criteria criteria = getSession().createCriteria(getPersistentClass());
            for (Criterion c : criterion) {
                criteria.add(c);
            }
            return (T) criteria.uniqueResult();
        } catch (HibernateException hibernateEx) {
            throw new DataLayerException("HibernateException", hibernateEx);
        } catch (RuntimeException runtimeEx) {
            throw new DataLayerException("RuntimeException", runtimeEx);
        }
    }

    /**
     * Returns a single result for a named query and parameters.
     * @param name The name of the query defined in metadata.
     * @param params A list of parameters for the named query.
     * @return A single result of type T or null if no results were found.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    @SuppressWarnings("unchecked")
	protected T findResult(final String name, final Parameter ...params) throws DataLayerException {
    	try {
    		Query query = getEntityManager().createNamedQuery(name);
    		for (Parameter param : params) {
    			param.addParameter(query);
    		}
    		return (T) query.getSingleResult();
    	} catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (NonUniqueResultException nonUniqueResultEx) {
        	throw new DataLayerException("NonUniqueResultException", nonUniqueResultEx);
        }  catch (NoResultException noResultEx) {
        	return null;
        }
    }

    /**
     * Returns the Entity Manager.
     * @return The Entity Manager.
     */
    protected EntityManager getEntityManager() {
        return this.entityManager;
    }

    /**
     * Returns the Class that is being persisted when extending this class.
     * @return The Class that is being persisted when extending this class.
     */
    protected Class<T> getPersistentClass() {
        return this.persistentClass;
    }

    /**
     * Returns the Hibernate session.
     * @return The Hibernate session.
     */
    protected Session getSession() {
        return (Session) entityManager.getDelegate();
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#insert(java.lang.Object)
     */
    public void insert(final T object) throws DataLayerException {
        try {
            getEntityManager().persist(object);
        } catch (EntityExistsException entityExistsEx) {
            throw new DataLayerException("EntityExistsException", entityExistsEx);
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (TransactionRequiredException transactionRequiredEx) {
            throw new DataLayerException("TransactionRequiredException", transactionRequiredEx);
        }
    }

    /**
     * Returns an object of type T by it's identifier.  An exception will be thrown if the object doesn't exist.
     * @param id The identifier for the object.
     * @return A object of type T.
     * @throws DataLayerException If an exception occurs in the data layer.
     */
    public T loadById(final Object id) throws DataLayerException {
        try {
            return getEntityManager().getReference(persistentClass, id);
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (EntityNotFoundException entityNotFoundEx) {
            throw new DataLayerException("EntityNotFoundException", entityNotFoundEx);
        }
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#refresh(java.lang.Object)
     */
    public void refresh(final T object) throws DataLayerException {
    	try {
    		getEntityManager().refresh(object);
    	} catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (TransactionRequiredException transactionRequiredEx) {
            throw new DataLayerException("TransactionRequiredException", transactionRequiredEx);
        } catch (EntityNotFoundException entityNotFoundEx) {
        	throw new DataLayerException("EntityNotFoundException", entityNotFoundEx);
        }
    }

    /**
     * Sets entityManager.
     * @param entityManager The entityManager to set.
     */
    @PersistenceContext @Inject
    public void setEntityManager(final EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * {@inheritDoc}
     * @see kiff.dao.CoreDAO#update(java.lang.Object)
     */
    public T update(final T object) throws DataLayerException {
        try {
            return getEntityManager().merge(object);
        } catch (IllegalStateException illegalStateEx) {
            throw new DataLayerException("IllegalStateException", illegalStateEx);
        } catch (IllegalArgumentException illegalArgumentEx) {
            throw new DataLayerException("IllegalArgumentException", illegalArgumentEx);
        } catch (TransactionRequiredException transactionRequiredEx) {
            throw new DataLayerException("TransactionRequiredException", transactionRequiredEx);
        }
    }
}
